package server;

import interfaces.SearchEngine;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Vector;
import persistence.XMLUtils;
import search.CompassSearchEngine;
import logger.ForumLogger;
import domain.items.ForumThread;
import domain.items.Item;
import domain.items.Post;
import domain.items.PostPacket;
import domain.items.ThreadPacket;
import domain.items.Topic;
import domain.items.TopicPacket;
import domain.users.Guest;
import domain.users.Member;
import domain.users.User;

/**
 * each incoming client is associated with an instance of WSEPHandler. this is
 * actually a thread. all the communication is made between WSEPConnection (on
 * the client side) and WSEPHandler (on the server side).
 */
public class WSEPHandler extends Thread {

	/**
	 * we map each functionality to a constant number. the client side
	 * (UserConnection) holds these numbers too.
	 *
	 * USAGE EXAMPLE: WSEPConnection.sendToSocket(""+LOGIN+" ayal 12345"); this
	 * string is passed through TCP WSEPHandler parses the string in method
	 * handleUserInput and invokes method the method 'login'.
	 *
	 */
	private static final int LOGIN = 0;
	private static final int REGISTER = 1;
	private static final int LOGOUT = 2;
	private static final int GET_ROOT_TOPIC = 3;
	private static final int GET_ITEM = 4;
	private static final int ADD_POST = 5;
	private static final int UPDATE_POST = 6;
	private static final int DELETE_POST = 7;
	private static final int ADD_THREAD = 8;
	private static final int DEMOTE_USER = 9;
	private static final int PROMOTE_USER = 10;
	private static final int GET_FATHER = 11;
	private static final int SEARCH_BY_AUTHOR = 12;
	private static final int SEARCH_BY_DATE = 13;
	private static final int SEARCH_BY_CONTENT = 14;
	private static final int DELETE_USER = 15;
	private static final int USERS_STATUS = 16;
	private static final int UPDATE_USER = 17;

	/**
	 * defines how to parse the messages sent through the socket
	 */
	private static final String DELIMITER = " \t ";

	/**
	 * the input network buffer
	 */
	private BufferedReader _in;

	/**
	 * the output network buffer
	 */
	private PrintWriter _out;

	/**
	 * the controller of the server (all instances of WsepHandler have reference
	 * to this single instance of the server)
	 */
	private ForumServer _server;

	/**
	 * this is the item being viewed by the surfer at this moment
	 */
	private Item _currentlyViewedItem;

	/**
	 * a Connection instance relates to a single user.
	 */
	private User _user;

	/**
	 * search engine for search queries
	 */
	private SearchEngine _searchEngine;

	/**
	 * constructor.
	 *
	 * @param acceptedSocket
	 * @param server
	 * @throws IOException
	 */
	public WSEPHandler(Socket acceptedSocket, ForumServer server) throws IOException {

		super();
		this._server = server;
		this._user = new Guest();
		_server.setNumOfCurrUsers(1);
		// this._searchEngine = new
		// WsepSearchEngine(this._server.getSearchDB());
		this._searchEngine = CompassSearchEngine.getInstance();
		this._in = new BufferedReader(new InputStreamReader(acceptedSocket.getInputStream()));
		this._out = new PrintWriter(acceptedSocket.getOutputStream(), true);
		System.out.println("Accepted connection from client!");
		System.out.println("Client is from: " + acceptedSocket.getInetAddress() + ":" + acceptedSocket.getPort());

	}

	/**
	 * this is what will run after connection is established.
	 */
	public void run() {
		while (true) {
			String input = this.readFromSocket();
			String reply = handleUserInput(input);
			this.sendToSocket(reply);
		}
	}

	/**
	 * handles the raw string received from the client. method invocation
	 * protocol: first word is the method number (according to mapping above)
	 * second word is first argument and so on...
	 *
	 * @param input
	 * @return
	 */
	private String handleUserInput(String input) {
		String reply = null;
		if (input.equals("")) {
			System.out.println("connection with client was lost!");
			try {
				_server.setNumOfCurrUsers(-1);
				this.join();
			} catch (InterruptedException e) {

			}
		}
		String[] tokens = input.split(DELIMITER);
		switch (Integer.parseInt(tokens[0])) {
		case LOGIN:
			reply = this.login(tokens[1], tokens[2]);
			break;
		case REGISTER:
			reply = this.registerUser(tokens[1], tokens[2],tokens[3]);
			break;
		case LOGOUT:
			reply = this.logout();
			break;
		case GET_ROOT_TOPIC:
			reply = this.getRootTopic();
			break;
		case GET_ITEM:
			reply = this.getItem(Integer.parseInt(tokens[1]));
			break;
		case ADD_POST:
			reply = this.addPost(tokens[1], tokens[2], tokens[3]);
			break;
		case ADD_THREAD:
			reply = this.addThread(tokens[1], tokens[2], tokens[3]);
			break;
		case GET_FATHER:
			reply = this.getFather();
			break;
		case UPDATE_POST:
			reply = this.updatePost(tokens[1], tokens[2]);
			break;
		case SEARCH_BY_AUTHOR:
			reply = this.searchByAuthor(tokens[1]);
			break;
		case SEARCH_BY_CONTENT:
			reply = this.searchByContent(tokens[1]);
			break;
		case SEARCH_BY_DATE:
			reply = this.searchByDate(tokens[1], tokens[2]);
			break;
		case DELETE_POST:
			reply = this.deletePost();
			break;
		case PROMOTE_USER:
			reply = this.promoteUser(tokens[1]);
			break;
		case DEMOTE_USER:
			reply = this.demoteUser(tokens[1]);
			break;
		case DELETE_USER:
			reply = this.deleteUser(tokens[1]);
			break;
		case USERS_STATUS:
			reply = _server.getCurrUsersStatus();
			break;
		case UPDATE_USER:
			reply = this.updateUser(tokens[1], tokens[2],tokens[3],tokens[4]);
			break;
		}
		return reply;
	}


	private String deleteUser(String username) {
		String ans = "";
		boolean success = this._server.getUserRepository().removeUser(username);
		if (success) {
			ans = "true";
		} else {
			ans = "ERROR";
		}
		return ans;
	}

	/**
	 * search by date. result is a serialized Vector<PostPacket>.
	 *
	 * @param fromDate
	 * @param toDate
	 * @return
	 */
	public String searchByDate(String fromDate, String toDate) {
			
		Vector<PostPacket> vecToSend = new Vector<PostPacket>();
		try {
			SimpleDateFormat tDateFormat = new SimpleDateFormat("dd/MM/yy");
			Date from = tDateFormat.parse(fromDate);
			Date to = tDateFormat.parse(toDate);
			
			Vector<Post> ans = this._searchEngine.searchByDate(from, to);
	
			for (Post post : ans) {
				vecToSend.add(post.convertToPacket());
			}
		}
		catch (java.text.ParseException e) {
			System.err.println("Unable to parse date for search");
		}
		return XMLUtils.serialize(vecToSend);
	}

	/**
	 * search by content. result is a serialized Vector<PostPacket>.
	 *
	 * @param sentence
	 * @return
	 */
	public String searchByContent(String sentence) {

		Vector<Post> ans = this._searchEngine.searchByContent(sentence);
		
		Vector<PostPacket> vecToSend = new Vector<PostPacket>();
		for (Post post : ans) {
			vecToSend.add(post.convertToPacket());
		}
		return XMLUtils.serialize(vecToSend);
	}

	/**
	 * search by author name. result is a serialized Vector<PostPacket>.
	 *
	 * @param authorName
	 * @return
	 */
	public String searchByAuthor(String authorName) {

		// // TEST #1 BEGIN - STUB VECTOR<POSTPACKET> RESULT TEST
		// PostPacket stubResult1 = new PostPacket("ayal","stub subject 1","stub
		// content 1");
		// PostPacket stubResult2 = new PostPacket("roee","stub subject 2","stub
		// content 2");
		// Vector<PostPacket> stubVector = new Vector<PostPacket>();
		// stubVector.add(stubResult1);
		// stubVector.add(stubResult2);
		// return XMLUtils.serialize(stubVector);
		//
		// // TEST #1 END

		// // TEST #2 BEGIN - STUB VECTOR<POST> RESULT TEST
		// Post stubResult1 = new Post(null,null,"stub subject 1","stub content
		// 1");
		// Post stubResult2 = new Post(null,null,"stub subject 2","stub content
		// 2");
		// Vector<Post> stubVector = new Vector<Post>();
		// stubVector.add(stubResult1);
		// stubVector.add(stubResult2);
		// Vector<Post> ans = stubVector;
		// // TEST #2 END

		// green-out this line for testing
		Vector<Post> ans = this._searchEngine.searchByAuthor(authorName);

		Vector<PostPacket> vecToSend = new Vector<PostPacket>();
		for (Post post : ans) {
			vecToSend.add(post.convertToPacket());
		}
		return XMLUtils.serialize(vecToSend);
	}

	/**
	 * used to step backwards.
	 *
	 * @return serialized item
	 */
	private String getFather() {
		Item father = this._currentlyViewedItem.getFather();
		String str = "";
		if (father == null) {
			str = "NULL";
			return str;
		}
		if (father instanceof Topic) {
			TopicPacket toReturn = ((Topic) father).convertToPacket();
			str = "ITEM" + DELIMITER + XMLUtils.serialize(toReturn);
		} else if (father instanceof ForumThread) {
			ThreadPacket toReturn = ((ForumThread) father).convertToPacket();
			str = "ITEM" + DELIMITER + XMLUtils.serialize(toReturn);
		} else {
			str = "ERROR" + DELIMITER + "could not retrieve father item from repository";
		}
		this._currentlyViewedItem = father;
		return str;
	}

	/**
	 * blocking method! returns a string sent from the other side of the
	 * connection.
	 *
	 * @return
	 */
	public String readFromSocket() {

		String response = "";
		char[] buffer = new char[2000];
		int charsRead = -2;
		try {
			while ((charsRead = this._in.read(buffer)) != -1) {
				if (charsRead <= 0)
					continue;
				response = new String(buffer, 0, charsRead);
				break;
			}
		} catch (IOException e) {
		}
		return response;

	}

	/**
	 * sends a string to the other side of the connection.
	 *
	 * @param string
	 */
	public void sendToSocket(String string) {
		this._out.print(string);
		this._out.flush();
	}

	/**
	 * increments user rank by 1
	 *
	 * @param username
	 * @return
	 */
	public String promoteUser(String username) {
		boolean success = this._server.getUserRepository().changeRank(username, 1);
		if (success) {
			return "true";
		} else {
			return "false";
		}
	}

	/**
	 * decrements user rank by 1
	 *
	 * @param username
	 * @return
	 */
	public String demoteUser(String username) {
		boolean success = this._server.getUserRepository().changeRank(username, -1);
		if (success) {
			return "true";
		} else {
			return "false";
		}
	}

	/**
	 * adds a post to the current thread being viewed
	 *
	 * @param subject
	 * @param text
	 * @param userName
	 * @return
	 */
	public String addPost(String subject, String text, String userName) {
		subject = scriptFilter(subject);
		text = scriptFilter(text);
		Post newPost = new Post(this._currentlyViewedItem, userName, subject, text);
		newPost.setDate(new Date());

		boolean success = this._server.getDataRepository().addPost(newPost);
		if (success) {
			this._searchEngine.updateSearchDB(newPost, null);
			@SuppressWarnings("unused")
			ThreadPacket temp = ((ForumThread) this._currentlyViewedItem).convertToPacket();
			ForumLogger.logAddPost(userName, subject);
			// return XMLUtils.serialize(temp);
			return "true";
		} else {
			ForumLogger.logAddPostFail(userName, subject);
			return "ERROR" + DELIMITER + "add new post failure";
		}

	}

	/**
	 * adds a thread to the current topic being viewed
	 *
	 * @param newMessageSubject
	 * @param newMessageText
	 * @param username
	 */
	public String addThread(String newMessageSubject, String newMessageText, String username) {
		newMessageSubject = scriptFilter(newMessageSubject);
		newMessageText = scriptFilter(newMessageText);
		String ans = "";
		ForumThread tempThread = new ForumThread(this._currentlyViewedItem);
		tempThread.setName(newMessageSubject);
		Post newPost = new Post(tempThread, username, newMessageSubject, newMessageText);
		// tempThread.getPosts().add(newPost);
		this._server.getDataRepository().addThread(tempThread);
		this._searchEngine.updateSearchDB(newPost, null);
		if (this._server.getDataRepository().addPost(newPost)) {
			// ForumLogger.logAddThread..... to ocmplete
			ans = "true";
		} else {
			// log failure...
			ans = "ERROR";
		}
		return ans;
	}

	/**
	 * delete the current post being viewed
	 *
	 * @return
	 */
	public String deletePost() {

		String tSubject = this._currentlyViewedItem.getName();
		Post oldPost = ((Post) (this._currentlyViewedItem)).clone();
		if (this._server.getDataRepository().deletePost((Post) this._currentlyViewedItem)) {
			this._searchEngine.updateSearchDB(null, oldPost);
			ForumLogger.logDeletePost(((Member)this._user).getUserName(), tSubject);
			return "true";
		} else {
			ForumLogger.logDeletePostFail(((Member)this._user).getUserName(), tSubject);
			return "false";
		}
	}

	/**
	 * get a sub-item according to choice number
	 *
	 * @param choice
	 * @return serialized item
	 */
	public String getItem(int choice) {
		// Topic topic = (Topic)(_currentlyViewedItem);
		Item item = _currentlyViewedItem;
		// topic.getSubItems().get(choice-1);
		// this.setCurrentlyViewedItem(item);
		if (item.getType() == domain.items.Item.TOPIC) {
			item = ((Topic) item).getSubItems().get(choice - 1);
			this.setCurrentlyViewedItem(item);
			if (item.getType() == domain.items.Item.TOPIC) {
				TopicPacket packet = ((Topic) item).convertToPacket();
				return XMLUtils.serialize(packet);
			}
			ThreadPacket packet = ((ForumThread) item).convertToPacket();
			return XMLUtils.serialize(packet);
		} else if (item.getType() == domain.items.Item.FORUM_THREAD) {
			item = ((ForumThread) item).getPosts().get(choice - 1);
			this.setCurrentlyViewedItem(item);
			PostPacket packet = ((Post) item).convertToPacket();
			return XMLUtils.serialize(packet);
		} else {
			System.out.println("WTF???????????");
			return "?WTF?";
		}
	}

	/**
	 * gets the root topic item
	 *
	 * @return serialized item
	 */
	public String getRootTopic() {
		this._currentlyViewedItem = this._server.getDataRepository().getRootTopic();
		TopicPacket packet = ((Topic) this._currentlyViewedItem).convertToPacket();
		return "ITEM" + DELIMITER + XMLUtils.serialize(packet);
	}

	public SearchEngine getSearchEngine() {
		// TODO Auto-generated method stub
		return this._searchEngine;
	}

	/**
	 *
	 * @param userName
	 * @param password
	 * @return
	 */
	public String login(String userName, String password) {

		if (this._server.getUserRepository().authenticate(userName, password)) {
			this._user = (Member) this._server.getUserRepository().getUser(userName);
			// ****logging:****
			ForumLogger.logLogin(userName);
			// ****************
			return "MEMBER" + DELIMITER + XMLUtils.serialize(_user);
		} else {
			// System.err.println("An Error Occurred On User Login.");
			ForumLogger.logLoginFail(userName);
			return "ERROR" + DELIMITER + "cannot find user record";
		}
	}

	/**
	 * switch to 'guest mode' surfing
	 *
	 * @return
	 */
	public String logout() {
		// ****logging:****
		 ForumLogger.logLogout(((Member)this._user).getUserName());
		// ****************
		this._user = new Guest();
		return "true";
	}

	/**
	 * add user to the repository
	 *
	 * @param username
	 * @param password
	 * @return
	 */
	public String registerUser(String username, String email, String password) {
		if (this._server.getUserRepository().addUser(username, email, password)) {
			ForumLogger.logRegisterUser(username);
			return "true";
		} else {
			ForumLogger.logRegisterFail(username);
			return "ERROR" + DELIMITER + "registeration failure";
		}
	}


	/**
	 * change the user's password
	 * @param username
	 * @param oldPass
	 * @param newPass
	 * @return
	 */
	private String updateUser(String username, String newEmail,String oldPass, String newPass) {
		if (this._server.getUserRepository().updateUser(username,newEmail,oldPass,newPass)){
			// log succ?
			return "true";
		} else {
			// log fail?
			return "ERROR" + DELIMITER + "update password failure";
		}
	}


	/**
	 * sets the currently viewed item
	 *
	 * @param item
	 * @return
	 */
	public boolean setCurrentlyViewedItem(Item item) {
		this._currentlyViewedItem = item;
		return true;
	}

	/**
	 * modifies an existing post
	 *
	 * @param newSubject
	 * @param newText
	 * @return
	 */
	public String updatePost(String newSubject, String newText) {
		newSubject = scriptFilter(newSubject);
		newText = scriptFilter(newText);
		String reply = "";
		String oldSubject = ((Post) this._currentlyViewedItem).getName();
		Post oldPost = ((Post) (this._currentlyViewedItem)).clone();
		((Post) this._currentlyViewedItem).setName(newSubject);
		((Post) this._currentlyViewedItem).setContent(newText);
		if (this._server.getDataRepository().updatePost((Post) this._currentlyViewedItem)) {
			reply = "true";
			this._searchEngine.updateSearchDB(((Post) this._currentlyViewedItem), oldPost);
			ForumLogger.logUpdatePost(((Member)this._user).getUserName(), newSubject, oldSubject);
		} else {
			reply = "ERROR" + DELIMITER + "update post failure";
			ForumLogger.logUpdatePostFail(((Member)this._user).getUserName(), newSubject, oldSubject);
		}
		return reply;
	}

	private String scriptFilter(String data){
		return data.replaceAll("<", "\"<\"");
	}

	public Item get_currentlyViewedItem() {
		return _currentlyViewedItem;
	}

	public void set_currentlyViewedItem(Item viewedItem) {
		_currentlyViewedItem = viewedItem;
	}

}
